package org.maxgroundstation.groundstation.view.map;

import java.awt.Color;
import java.awt.Cursor;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.awt.geom.AffineTransform;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import javax.swing.JFrame;
import javax.swing.JPanel;

import org.maxgroundstation.groundstation.GroundStation;
import org.maxgroundstation.groundstation.GroundStationEvent;
import org.maxgroundstation.groundstation.GroundStationListener;
import org.maxgroundstation.groundstation.comm.service.ServiceEvent;
import org.maxgroundstation.groundstation.comm.service.ServiceListener;
import org.maxgroundstation.groundstation.comm.service.geometry.GeometryService;
import org.maxgroundstation.groundstation.comm.service.gps.GpsService;
import org.maxgroundstation.groundstation.comm.service.gps.PointOfInterest;
import org.maxgroundstation.world.WorldModel;
import org.maxgroundstation.world.landmark.Landmark;

/**
 * A panel for viewing a local GPS map.
 * @author Scott Davis
 * 
 * Copyright (C) 2010  Scott Davis
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
@SuppressWarnings("serial")
public class MapViewPanel extends JPanel implements GroundStationListener, 
        ServiceListener {
    
    // Static members.
    public final static float DEFAULT_MAP_ZOOM = .2F;
    
    // Data members.
    private GroundStation gs;
    private BaseSceneMap baseMap;
    private Map<String, MapOverlay> mapOverlays;
    private Map<String, Boolean> displayMapOverlays;
    private GpsLocation currentLoc;
    private float zoom;
    private double xLast;
    private double yLast;
    private double xPos;
    private double yPos;
    private List<PointOfInterest> poiCache;
    private String baseMapStyle = "";
    private boolean isFullMapRender;
    private boolean isMouseDrag;
    
    /**
     * Constructor
     * @param gs the ground station.
     */
    public MapViewPanel(GroundStation gs) {
        // Use JPanel constructor.
        super();
        
        // Initialize data members.
        this.gs = gs;
        baseMap = new BaseSceneMap(gs, this);
        mapOverlays = createMapOverlays();
        displayMapOverlays = createDisplayMapOverlays();
        currentLoc = null;
        zoom = DEFAULT_MAP_ZOOM;
        xPos = 0D;
        yPos = 0D;
        poiCache = new ArrayList<PointOfInterest>();
        isFullMapRender = false;
        isMouseDrag = false;
        
        // Set background color.
        setBackground(Color.BLACK);
        setOpaque(true);
        
        // Set cursor to move cursor.
        setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
        
        // Add mouse listener.
        addMouseListener(new MouseAdapter() {
            public void mousePressed(MouseEvent evt) {
                // Set initial mouse drag position.
                xLast = evt.getX();
                yLast = evt.getY();
            }
            
            public void mouseClicked(MouseEvent evt) {
                if (currentLoc != null) {
                    // Open dialogs for any point of interests clicked on.
                    if (isDisplayMapOverlay(PointsOfInterestMapOverlay.OVERLAY_ID)) {
                        openPointOfInterestDialogs(evt.getX(), evt.getY());
                    }
                    
                    // Open dialogs for any landmarks clicked on.
                    if (isDisplayMapOverlay(LandmarksMapOverlay.OVERLAY_ID)) {
                        openLandmarkDialogs(evt.getX(), evt.getY());
                    }
                }
            }
            
            public void mouseReleased(MouseEvent evt) {
                // Set flag to make sure map fully renders when mouse is released
                // after mouse dragging.
                if (isMouseDrag) {
                    isFullMapRender = true;
                    isMouseDrag = false;
                }
            }
        });
        
        // Add mouse motion listener.
        addMouseMotionListener(new MouseMotionAdapter() {
            @Override
            public void mouseDragged(MouseEvent evt) {
                // Move map center based on mouse drag difference.
                double xDiff = evt.getX() - xLast;
                double yDiff = evt.getY() - yLast;
                moveCenter(xDiff, yDiff);
                xLast = evt.getX();
                yLast = evt.getY();
                isMouseDrag = true;
            }
        });
        
        // Add as ground station listener.
        gs.addGroundStationListener(this);
    }
    
    /**
     * Set the base map style.
     * @param setTo base map style to use.
     */
    public void setBaseMapStyle(String setTo) {
        baseMapStyle = setTo;
    }
    
    /**
     * Gets the base map style as a string.
     * @return base map style.
     */
    public String getBaseMapStyle() {
        return baseMapStyle;
    }
    
    /**
     * Gets the ground station.
     * @return ground station.
     */
    public GroundStation getGroundStation() {
        return gs;
    }
    
    /**
     * Creates the map overlays map.
     * @return map of all map overlays.
     */
    private Map<String, MapOverlay> createMapOverlays() {
        Map<String, MapOverlay> result = new LinkedHashMap<String, MapOverlay>();
        result.put(RoverPathMapOverlay.OVERLAY_ID, new RoverPathMapOverlay(gs));
        result.put(RoverMapOverlay.OVERLAY_ID, new RoverMapOverlay(gs));
        result.put(RoverHeadingMapOverlay.OVERLAY_ID, new RoverHeadingMapOverlay(gs));
        result.put(LandmarksMapOverlay.OVERLAY_ID, new LandmarksMapOverlay(gs));
        result.put(LandmarkLabelsMapOverlay.OVERLAY_ID, new LandmarkLabelsMapOverlay(gs));
        result.put(PointsOfInterestMapOverlay.OVERLAY_ID, new PointsOfInterestMapOverlay(gs));
        result.put(PointOfInterestLabelsMapOverlay.OVERLAY_ID, new PointOfInterestLabelsMapOverlay(gs));
        result.put(NorthIndicatorMapOverlay.OVERLAY_ID, new NorthIndicatorMapOverlay());
        result.put(ScaleMapOverlay.OVERLAY_ID, new ScaleMapOverlay());
        result.put(GpsInformationMapOverlay.OVERLAY_ID, new GpsInformationMapOverlay(gs));
        result.put(GeometryMapOverlay.OVERLAY_ID, new GeometryMapOverlay(gs));
        
        return result;
    }
    
    /**
     * Creates the display map overlays map.
     * @return map of all display map overlays set initially to true.
     */
    private Map<String, Boolean> createDisplayMapOverlays() {
        int overlayNum = mapOverlays.size();
        Map<String, Boolean> result = new LinkedHashMap<String, Boolean>(overlayNum);
        Iterator<String> i = mapOverlays.keySet().iterator();
        while (i.hasNext()) {
            result.put(i.next(), true);
        }
        return result;
    }
    
    /**
     * Checks if a map overlay is to be displayed.
     * @param mapOverlayID the map overlay ID.
     * @return true if map overlay is to be displayed.
     */
    public boolean isDisplayMapOverlay(String mapOverlayID) {
        boolean result = false;
        if (displayMapOverlays.containsKey(mapOverlayID)) {
            result = displayMapOverlays.get(mapOverlayID);
        }
        return result;
    }
    
    /**
     * Sets if a map overlay is to be displayed.
     * @param mapOverlayID the map overlay ID.
     * @param display true if map overlay is to be displayed.
     */
    public void setDisplayMapOverlay(String mapOverlayID, boolean display) {
        if (displayMapOverlays.containsKey(mapOverlayID)) {
            displayMapOverlays.put(mapOverlayID, display);
        }
    }
    
    /**
     * Gets a list of all map overlay IDs.
     * @return list of map overlay ID strings.
     */
    public List<String> getMapOverlayIDs() {
        return new ArrayList<String>(mapOverlays.keySet());
    }

    /**
     * Checks if the GPS service exists.
     * @return true if GPS service exists.
     */
    private boolean hasGpsService() {
        return gs.getServiceComm().hasService(GpsService.SERVICE_ID);
    }
    
    /**
     * Gets the GPS service.
     * @return GPS service or null if not available.
     */
    private GpsService getGpsService() {
        GpsService service = null;
        if (hasGpsService()) {
            service = (GpsService) gs.getServiceComm().getService(
                    GpsService.SERVICE_ID);
        }
        return service;
    }
    
    /**
     * Checks if the geometry service exists.
     * @return true if geometry service exists.
     */
    private boolean hasGeometryService() {
        return gs.getServiceComm().hasService(GeometryService.SERVICE_ID);
    }
    
    /**
     * Gets the geometry service.
     * @return geometry service or null if not available.
     */
    private GeometryService getGeometryService() {
        GeometryService service = null;
        if (hasGeometryService()) {
            service = (GeometryService) gs.getServiceComm().getService(
                    GeometryService.SERVICE_ID);
        }
        return service;
    }

    @Override
    public void groundStationUpdate(GroundStationEvent event) {
        // If services initialized from robot, check for GPS service.
        if (GroundStationEvent.SERVICES_INITIALIZED.equals(event.getType())) {
            if (hasGpsService()) {
                // Add as GPS service listener.
                getGpsService().addServiceListener(this);
                
                // Populate points of interest cache.
                poiCache = getGpsService().getPointsOfInterest();
            }
            
            if (hasGeometryService()) {
                // Add as geometry service listener.
                getGeometryService().addServiceListener(this);
            }
        }
    }

    @Override
    public void serviceUpdate(ServiceEvent event) {
        
        if (event.getType().equals(GpsService.GPS_UPDATE)) {
            // Update current location with new GPS location.
            Map<String, String> gpsInfo = (Map<String, String>) event.getTarget();
            double latitude = Double.parseDouble(gpsInfo.get("lat"));
            double longitude = Double.parseDouble(gpsInfo.get("lon"));
            currentLoc = new GpsLocation(latitude, longitude);
        }
        else if (event.getType().equals(GpsService.POI_UPDATE)) {
            // Update point of interest cache from GPS service.
            poiCache = (List<PointOfInterest>) event.getTarget();
        }
        
        // Redraw map.
        repaint();
    }
    
    @Override
    public void paintComponent(Graphics g) {
        super.paintComponent(g);
        
        Graphics2D g2d = (Graphics2D) g;
        
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, 
                RenderingHints.VALUE_ANTIALIAS_ON);
        
        // Draw base map.
        if (currentLoc != null) {
            GpsLocation mapLoc = MapUtil.getNewGpsLocation(xPos * zoom * -1D, yPos * zoom, currentLoc);
            baseMap.drawMap(baseMapStyle, g2d, mapLoc, getWidth(), getHeight(), zoom, isFullMapRender);
            isFullMapRender = false;
        }
        
        // Draw all map overlays.
        Iterator<String> j = displayMapOverlays.keySet().iterator();
        while (j.hasNext()) {
            String mapOverlayID = j.next();
            if (displayMapOverlays.get(mapOverlayID)) {
                MapOverlay mapOverlay = mapOverlays.get(mapOverlayID);
                AffineTransform saveTransform = g2d.getTransform();
                
                // Translate map from modified center point.
                if (mapOverlay.isDraggable()) g2d.translate(xPos, yPos);
                
                // Draw map overlay.
                mapOverlay.drawMapOverlay(baseMapStyle, g2d, currentLoc, getWidth(), getHeight(), zoom);
                
                g2d.setTransform(saveTransform);
            }
        }
    }
    
    /**
     * Gets the map zoom.
     * @return zoom (m/pixel).
     */
    public float getZoom() {
        return zoom;
    }
    
    /**
     * Sets the map zoom.
     * @param zoom the zoom (m/pixel).
     */
    public void setZoom(float zoom) {
        float zoomChange = zoom / this.zoom;
        xPos /= zoomChange;
        yPos /= zoomChange;
        this.zoom = zoom;
        repaint();
    }
    
    /**
     * Moves the center of the map by a given number of pixels.
     * @param xDiff the X axis pixels.
     * @param yDiff the Y axis pixels.
     */
    private void moveCenter(double xDiff, double yDiff) {
        xPos += xDiff;
        yPos += yDiff;
        repaint();
    }
    
    /**
     * Recenters the map on the rover.
     */
    public void recenterMap() {
        xPos = 0D;
        yPos = 0D;
        repaint();
    }
    
    /**
     * Gets the GPS location of an x,y point on the map.
     * @param x the x position.
     * @param y the y position.
     * @return the GPS location.
     */
    private GpsLocation getMapLocation(int x, int y) {
        int xCenter = getWidth() / 2;
        int yCenter = getHeight() / 2;
        double xOffset = (double) (x - xCenter - xPos) * zoom;
        double yOffset = (double) (y - yCenter - yPos) * zoom;
        return MapUtil.getNewGpsLocation(xOffset, (yOffset * -1D), currentLoc);
    }
    
    /**
     * Opens dialogs for any points of interest in range of a map location.
     * @param x the x position.
     * @param y the y position.
     */
    private void openPointOfInterestDialogs(int x, int y) {
        GpsLocation loc = getMapLocation(x, y);
        //System.out.println("Map clicked on lat: " + loc.getLatitude() + ", lon: " + loc.getLongitude());
        Iterator<PointOfInterest> i = poiCache.iterator();
        while (i.hasNext()) {
            PointOfInterest poi = i.next();
            GpsLocation poiLoc = new GpsLocation(poi.getLatitude(), poi.getLongitude());
            double distance = MapUtil.getDistance(loc, poiLoc);
            //System.out.println("Distance from " + poi.getName() + ": " + distance);
            if ((distance / zoom) < 10D) {
                //System.out.println("Open " + poi.getName() + " dialog");
                JFrame topFrame = (JFrame) getTopLevelAncestor();
                PointOfInterestDialog dialog = new PointOfInterestDialog(topFrame, poi, gs);
                dialog.setVisible(true);
            }
        }
    }
    
    /**
     * Opens dialogs for any landmarks in range of a map location.
     * @param x the x position.
     * @param y the y position.
     */
    private void openLandmarkDialogs(int x, int y) {
        GpsLocation loc = getMapLocation(x, y);
        WorldModel worldModel = gs.getWorldModel();
        if (worldModel != null) {
            Iterator<Landmark> i = worldModel.getLandmarks().iterator();
            while (i.hasNext()) {
                Landmark landmark = i.next();
                GpsLocation landmarkLoc = new GpsLocation(landmark.getLatitude(), landmark.getLongitude());
                double distance = MapUtil.getDistance(loc, landmarkLoc);
                if ((distance / zoom) < 10D) {
                    JFrame topFrame = (JFrame) getTopLevelAncestor();
                    LandmarkDialog dialog = new LandmarkDialog(topFrame, landmark);
                    dialog.setVisible(true);
                }
            }
        }
    }
}